﻿using System;
using System.Drawing;
using System.Runtime.InteropServices;
using System.Runtime.InteropServices.ComTypes;
using System.Text;
using System.Windows.Forms;
using static Microsoft.Win32.NativeMethods;

namespace Microsoft.Win32
{
	/// <summary>
	/// Display modes for a link.
	/// </summary>
	public enum LinkDisplayMode : uint
	{
		Normal = 1,
		Minimized = 7,
		Maximized = 3
	}

	/// <summary>
	/// Flags determining how the links with missing targets are resolved.
	/// </summary>
	[Flags]
	public enum LinkResolution : uint
	{
		None = 0,

		/// <summary>
		/// Do not display a dialog box if the link cannot be resolved. When NoUI is set, a time-out value that specifies the
		/// maximum amount of time to be spent resolving the link can be specified in milliseconds. The function returns if the
		/// link cannot be resolved within the time-out duration. If the timeout is not set, the time-out duration will be
		/// set to the default value of 3,000 milliseconds (3 seconds).
		/// </summary>
		NoUI = 0x1,

		/// <summary>Allow any match during resolution. Has no effect on ME/2000 or above, use the other flags instead.</summary>
		AnyMatch = 0x2,

		/// <summary>
		/// If the link object has changed, update its path and list of identifiers. If UPDATE is set, you do not need to
		/// call IPersistFile::IsDirty to determine whether or not the link object has changed.
		/// </summary>
		Update = 0x4,

		/// <summary>Do not update the link information.</summary>
		NoUpdate = 0x8,

		/// <summary>Do not execute the search heuristics.</summary>
		NoSearch = 0x10,

		/// <summary>Do not use distributed link tracking.</summary>
		NoTrack = 0x20,

		/// <summary>
		/// Disable distributed link tracking. By default, distributed link tracking tracks removable media
		/// across multiple devices based on the volume name. It also uses the UNC path to track remote file
		/// systems whose drive letter has changed. Setting NoLinkInfo disables both types of tracking.
		/// </summary>
		NoLinkInfo = 0x40,

		/// <summary>Call the Microsoft Windows Installer.</summary>
		InvokeMSI = 0x80,

		/// <summary>Windows XP and later. Assume same as NoUI but intended for applications without a hWnd.</summary>
		NoUIWithMsgPump = 0x101,

		/// <summary>Windows 7 and later. Offer the option to delete the shortcut when this method is unable to resolve it, even if the shortcut is not a shortcut to a file.</summary>
		OfferDeleteWithoutFile = 0x200,

		/// <summary>Windows 7 and later. Report as dirty if the target is a known folder and the known folder was redirected. This only works if the original target path was a file system path or ID list and not an aliased known folder ID list.</summary>
		KnonnFolder = 0x400,

		/// <summary>Windows 7 and later. Resolve the computer name in UNC targets that point to a local computer. This value is used with SLDFKEEPLOCALIDLISTFORUNCTARGET.</summary>
		MachineInLocalTarget = 0x800,

		/// <summary>Windows 7 and later. Update the computer GUID and user SID if necessary.</summary>
		UpdateMachineAndSid = 0x1000,

		/// <summary>?? Assuming this does not update the Object ID</summary>
		NoObjectID = 0x2000
	}

	/// <summary>
	/// Summary description for ShellLink.
	/// </summary>
	public class ShellLink : IDisposable
	{
		private IShellLinkW link;
		private string shortcutFile = "";

		/// <summary>
		/// Creates an instance of the Shell Link object.
		/// </summary>
		public ShellLink()
		{
			link = (IShellLinkW)new CShellLinkW();
		}

		/// <summary>
		/// Creates an instance of a Shell Link object from the specified link file
		/// </summary>
		/// <param name="linkFile">The Shortcut file to open</param>
		public ShellLink(string linkFile) : this()
		{
			Open(linkFile);
		}

		/// <summary>
		/// Call dispose just in case it hasn't happened yet
		/// </summary>
		~ShellLink()
		{
			Dispose();
		}

		/// <summary>
		/// Gets/sets any command line arguments associated with the link
		/// </summary>
		public string Arguments
		{
			get
			{
				StringBuilder arguments = new StringBuilder(260, 260);
				link.GetArguments(arguments, arguments.Capacity);
				return arguments.ToString();
			}
			set
			{
				link.SetArguments(value);
			}
		}

		/// <summary>
		/// Gets/sets the description of the link
		/// </summary>
		public string Description
		{
			get
			{
				StringBuilder description = new StringBuilder(1024, 1024);
				link.GetDescription(description, description.Capacity);
				return description.ToString();
			}
			set
			{
				link.SetDescription(value);
			}
		}

		/// <summary>
		/// Gets/sets the initial display mode when the shortcut is
		/// run
		/// </summary>
		public LinkDisplayMode DisplayMode
		{
			get { return (LinkDisplayMode)link.GetShowCmd(); }
			set { link.SetShowCmd((uint)value); }
		}

		/// <summary>
		/// Gets/sets the HotKey to start the shortcut (if any)
		/// </summary>
		public Keys HotKey
		{
			get { return (Keys)link.GetHotKey(); }
			set { link.SetHotKey((short)value); }
		}

		/// <summary>
		/// Gets the index of this icon within the icon path's resources
		/// </summary>
		public int IconIndex
		{
			get
			{
				int iconIndex = 0;
				GetIconLocation(out iconIndex);
				return iconIndex;
			}
			set
			{
				int iconIndex = 0;
				link.SetIconLocation(GetIconLocation(out iconIndex), value);
			}
		}

		private string GetIconLocation(out int iconIndex)
		{
			StringBuilder iconPath = new StringBuilder(260, 260);
			iconIndex = 0;
			link.GetIconLocation(iconPath, iconPath.Capacity, out iconIndex);
			return iconPath.ToString();
		}

		/// <summary>
		/// Gets the path to the file containing the icon for this shortcut.
		/// </summary>
		public string IconPath
		{
			get
			{
				int iconIndex = 0;
				return GetIconLocation(out iconIndex);
			}
			set
			{
				int iconIndex = 0;
				GetIconLocation(out iconIndex);
				link.SetIconLocation(value, iconIndex);
			}
		}

		/// <summary>Get or sets the list of item identifiers for a Shell link.</summary>
		public IntPtr IDList
		{
			get { return link.GetIDList(); }
			set { link.SetIDList(value); }
		}

		/// <summary>
		/// Gets a System.Drawing.Icon containing the icon for this
		/// ShellLink object.
		/// </summary>
		public Icon LargeIcon => GetIcon(true);

		/// <summary>
		/// Gets/sets the fully qualified path to the link's target
		/// </summary>
		public string Path
		{
			get { return GetPath(SLGP.UNCPRIORITY); }
			set { link.SetPath(value); }
		}

		/// <summary>
		/// Gets/sets the relative path to the link's target
		/// </summary>
		public string RelativePath
		{
			get { return GetPath(SLGP.RELATIVEPRIORITY); }
			set { link.SetRelativePath(value, 0); }
		}

		public string ShortcutFile
		{
			get { return shortcutFile; }
			set { shortcutFile = value; }
		}

		/// <summary>
		/// Gets/sets the short (8.3 format) path to the link's target
		/// </summary>
		public string ShortPath => GetPath(SLGP.SHORTPATH);

		private string GetPath(SLGP value)
		{
			StringBuilder target = new StringBuilder(260, 260);
			WIN32_FIND_DATA fd = new WIN32_FIND_DATA();
			link.GetPath(target, target.Capacity, fd, value);
			return target.ToString();
		}

		public Icon SmallIcon => GetIcon(false);

		/// <summary>
		/// Gets/sets the Working Directory for the Link
		/// </summary>
		public string WorkingDirectory
		{
			get
			{
				StringBuilder path = new StringBuilder(260, 260);
				link.GetWorkingDirectory(path, path.Capacity);
				return path.ToString();
			}
			set
			{
				link.SetWorkingDirectory(value);
			}
		}

		/// <summary>
		/// Dispose the object, releasing the COM ShellLink object
		/// </summary>
		public void Dispose()
		{
			if (link != null)
			{
				Marshal.ReleaseComObject(link);
				link = null;
			}
		}

		/// <summary>
		/// Loads a shortcut from the specified file
		/// </summary>
		/// <param name="linkFile">The shortcut file (.lnk) to load</param>
		/// <param name="window">The window that the Shell will use as the parent for a dialog box. The Shell displays the dialog box if it needs to prompt the user for more information while resolving a Shell link.</param>
		/// <param name="resolveFlags">The resolve flags.</param>
		/// <param name="timeOut">The time out.</param>
		public void Open(string linkFile, IWin32Window window = null, LinkResolution resolveFlags = LinkResolution.AnyMatch | LinkResolution.NoUI, ushort timeOut = 1)
		{
			if ((resolveFlags & LinkResolution.NoUI) == LinkResolution.NoUI)
				resolveFlags |= (LinkResolution)(timeOut << 16);

			((IPersistFile)link).Load(linkFile, 0); //STGM_DIRECT)
			link.Resolve(window == null ? IntPtr.Zero : window.Handle, (uint)resolveFlags);
			shortcutFile = linkFile;
		}

		/// <summary>
		/// Saves the shortcut to ShortCutFile.
		/// </summary>
		public void Save()
		{
			Save(shortcutFile);
		}

		/// <summary>
		/// Saves the shortcut to the specified file
		/// </summary>
		/// <param name="linkFile">The shortcut file (.lnk)</param>
		public void Save(string linkFile)
		{
			((IPersistFile)link).Save(linkFile, true);
			shortcutFile = linkFile;
		}

		public override bool Equals(object obj)
		{
			ShellLink link2 = obj as ShellLink;
			if (link2 != null)
				return link2.ToString() == ToString();
			return base.Equals(obj);
		}

		public override int GetHashCode() => ToString().GetHashCode();

		public override string ToString()
		{
			var pathBuilder = new StringBuilder(260);
			link.GetPath(pathBuilder, pathBuilder.Capacity, null, SLGP.RAWPATH);

			string title = null;
			// Need to use the property store to get the title for the link.
			using (PROPVARIANT pv = new PROPVARIANT())
			{
				var propStore = (IPropertyStore)link;
				PKEY pkeyTitle = PKEY.Title;

				propStore.GetValue(new REFPROPERTYKEY(PKEY.Title.Key), pv);

				// PKEY_Title should be an LPWSTR if it's not empty.
				title = pv.Value?.ToString() ?? "";
			}

			var argsBuilder = new StringBuilder(0x400);
			link.GetArguments(argsBuilder, argsBuilder.Capacity);

			// Path and title should be case insensitive.
			// Shell treats arguments as case sensitive because apps can handle those differently.
			return pathBuilder.ToString().ToUpperInvariant() + title.ToUpperInvariant() + argsBuilder.ToString();
		}

		private Icon GetIcon(bool large)
		{
			// Get icon index and path:
			int iconIndex = 0;
			StringBuilder iconPath = new StringBuilder(260, 260);
			link.GetIconLocation(iconPath, iconPath.Capacity, out iconIndex);
			string iconFile = iconPath.ToString();

			// If there are no details set for the icon, then we must use the shell to get the icon for the target
			if (iconFile.Length == 0)
			{
				ShellFileInfo sfi = new ShellFileInfo(Path);
				return large ? sfi.LargeIcon : sfi.SmallIcon;
			}
			else
			{
				// Use ExtractIconEx to get the icon:
				IntPtr[] hIconEx = new IntPtr[1] { IntPtr.Zero };
				int iconCount = 0;
				if (large)
					iconCount = ExtractIconEx(iconFile, iconIndex, hIconEx, null, 1);
				else
					iconCount = ExtractIconEx(iconFile, iconIndex, null, hIconEx, 1);
				// If success then return as a GDI+ object
				Icon icon = null;
				if (hIconEx[0] != IntPtr.Zero)
					icon = ShellFileInfo.GetClonedIcon(hIconEx[0]);
				return icon;
			}
		}
	}
}